Wireguard für eine Flotte von Raspi-road-warriors – und der Haken mit ufw
Für das erstmalige Einrichten eines Wireguard-VPN-Servers finden sich ja eine
Vielzahl von Anleitungen im Internet, darum hier nur in aller Kürze. Das verwendete Setup
sieht einen VPN-Server vor, mit dem alle Raspis als Clients verbinden.
Als ersten
Schritt installieren wir das wireguard Paket:
apt install wireguard
Anschließend die Datei /etc/wireguard/wg0.conf mit folgendem Inhalt anlegen:
[Interface]
Address = 10.10.100.5/24
SaveConfig = true
ListenPort = 51820
Dann wird ein private key erzeugt und dieser Konfigurationsdatei hinzugefügt:
{ echo -n "PrivateKey = " ; wg genkey; } | tee -a /etc/wireguard/wg0.conf
Jetzt kann das Wireguard-Interface gestartet werden:
wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.10.100.5/24 dev wg0
[#] ip link set mtu 1420 up dev wg0
Damit ist unser Server betriebsbereit.
Die Konfiguration für einen Raspi könnte so aussehen:
[Interface]
Address = 10.10.100.20/32
PrivateKey = <private_key_vom_raspi>
[Peer]
PublicKey = XzF1p6qb8zEBXWgI1b8Mqv4Z9qksTU/tTIEFAROMHEo=
Endpoint = 99.22.11.22:51820
# Route only vpn trafic through vpn
AllowedIPs = 10.10.100.0/24
# Route ALL traffic through vpn
#AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 21
wg set wg0 peer <public_key_vom_raspi> = allowed-ips 10.10.100.20/32
wg-quick save wg0 # auf der disk
Erweitertes VPN Setup #
Die erste Version der Netzwerktopologie war noch recht einfach gehalten: Eine überschaubare Anzahl von RaspberryPi Devices (ca. 20) verbinden sich mit einem zentralen Wireguard-VPN-Server und liefern dort Nutzdaten und Performancedaten ab; quasi ein sternförmiges Setup, mit dem Server in der Mitte. Nachdem sich die Anwendung als sehr beliebt herausgestellt hat und immer mehr Daten erhoben wurden wollte ich die REST-API für die Datenerhebung physikalisch vom VPN-Server und dem Analyse-Server trennen. Es kommen damit zwei peers hinzu, die von den Raspis erreicht werden sollen.
10.10.100.5"]] API["API-Server
10.10.100.30"] ANALYSE["Analyse-Server
10.10.100.31"] R1 -->|wg0| VPN R2 -->|wg0| VPN R3 -->|wg0| VPN VPN -->|FORWARD -d 10.10.100.30| API VPN -->|FORWARD -d 10.10.100.31| ANALYSE
API- und Analyse-Server laufen also nicht mehr auf dem VPN-Server selbst, sondern als eigene Hosts, die über den VPN-Server erreicht werden – der wird damit vom Endpunkt zum Router zwischen den Raspis und den beiden neuen Peers.
Eigentlich auch kein Problem: Die beiden neuen Server bekommen eine eigene VPN-Konfiguration und verbinden mit dem Wireguard-Server. Auf diesem Server muss jetzt noch die Konfiguration so angepasst werden, dass die eingehenden Pakete weitergeleitet werden können.
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; \
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; \
ip6tables -A FORWARD -i wg0 -j ACCEPT; \
ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; \
iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; \
ip6tables -D FORWARD -i wg0 -j ACCEPT; \
ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
Die Firewall #
Bis hierher lauscht der Server auf genau einem Port, 51820/udp, für den
WireGuard-Handshake – und sonst reicht wer immer will an der Kiste vorbei. Das
wollte ich nicht dem Zufall überlassen und habe ufw
draufgesetzt, weil es die paar iptables-Regeln, die ich wirklich brauche, in
eine lesbare Zeile pro Regel übersetzt:
apt install ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 51820/udp
ufw allow OpenSSH
ufw enable
ufw status verbose zeigt danach brav Default: deny (incoming), allow (outgoing), deny (routed) – und genau bei diesem letzten Wort, routed, habe
ich mich zu früh gefreut.
Der Haken #
Meine Annahme war: deny (routed) heißt, ein Raspi kommt an keinen anderen
Peer heran, außer ich erlaube es explizit mit ufw route allow. Ein ping
von einem Raspi zum nächsten widerlegt das sofort – die Weiterleitung
funktioniert klaglos, obwohl ich dafür nie eine ufw-Regel gesetzt habe.
Der Grund steckt im PostUp von weiter oben. wg-quick hängt sein
iptables -A FORWARD -i wg0 -j ACCEPT direkt an die laufende Kernel-Chain, an
ufw und dessen Konfigurationsdateien vorbei. iptables -L FORWARD -n --line-numbers zeigt, wo die Regel landet:
Chain FORWARD (policy DROP)
num target prot opt in out source destination
3 ufw-before-logging-forward all -- * * 0.0.0.0/0 0.0.0.0/0
4 ufw-before-forward all -- * * 0.0.0.0/0 0.0.0.0/0
5 ufw-after-forward all -- * * 0.0.0.0/0 0.0.0.0/0
...
9 ACCEPT all -- wg0 * 0.0.0.0/0 0.0.0.0/0
Meine eigene Regel läuft als letzte Zeile der Chain, nach allem, was ufw
selbst verwaltet – und ein pauschales ACCEPT für alles von wg0 gewinnt
immer, egal was ufw route allow/deny weiter oben dazu sagen würde. Ein
ufw reload räumt das nicht auf, weil wg-quick diese Regel bei jedem
wg-quick up wg0 ohnehin neu setzt, ufw-Zustand hin oder her.
Für einen VPN mit einer Handvoll vertrauenswürdiger Peers ist das egal. Für
den Hub mit ca. 20 Raspis, von denen ich keinem hundertprozentig traue, ist es
das nicht: AllowedIPs verhindert nur, dass ein Raspi die Absenderadresse
eines anderen benutzt – wohin sein Paket danach darf, sagt das nicht. Ein
kompromittierter Raspi käme also ungehindert bei allen anderen vorbei.
Gezielt statt pauschal #
Die Lösung liegt nicht in ufw, sondern darin, das pauschale ACCEPT im
eigenen PostUp durch die tatsächlich gebrauchten Ziele zu ersetzen – jeder
Raspi darf zum API- und zum Analyse-Server, aber zu keinem anderen Peer:
PostUp = iptables -A FORWARD -i wg0 -d 10.10.100.30 -j ACCEPT; \
iptables -A FORWARD -i wg0 -d 10.10.100.31 -j ACCEPT; \
iptables -A FORWARD -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; \
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -d 10.10.100.30 -j ACCEPT; \
iptables -D FORWARD -i wg0 -d 10.10.100.31 -j ACCEPT; \
iptables -D FORWARD -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; \
iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
10.10.100.30 und .31 sind API- und Analyse-Server aus dem “Erweitertes
VPN Setup” oben. Die dritte Zeile lässt nur noch die Antworten auf diese
beiden Verbindungen zurück zum jeweiligen Raspi – ohne sie könnte der
API-Server zwar antworten, aber nichts von sich aus zu einem Raspi öffnen, was
für dieses Setup ohnehin nie gebraucht wird. Ein FORWARD von Raspi zu Raspi
taucht in der Chain gar nicht mehr auf und landet damit in der
Default-DROP-Policy.
Fazit #
ufw ist nach wie vor die richtige Wahl, um den Server selbst dichtzumachen –
ein offener Port für den Handshake, der Rest zu. Für das Verhalten zwischen
den Peers ist es aber die falsche Stelle: Das entscheidet WireGuards eigenes
PostUp/PostDown, bevor ufw überhaupt zum Zug kommt. Wer eine Flotte von
Geräten aufbaut, die sich gegenseitig nicht sehen sollen, muss dort ansetzen –
nicht bei ufw route. 🔒